package com.liuwa.common.utils;

import com.liuwa.common.core.redis.RedisCache;
import com.liuwa.common.utils.spring.SpringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit;

/**
 * 分布式锁工具
 */
public class LockUtils {

    /**
     * 永不过期
     */
    private static final long PERSIST = -1;

    /**
     * 锁刷新队列(用来定时续签)
     */
    private static final ConcurrentMap<String, LockTime> REFRESH_QUEUE = new ConcurrentHashMap<String, LockTime>();

    private static Logger logger = LoggerFactory.getLogger(LockUtils.class);

    /**
     * 等待超时毫秒数
     */
    private static final int WAIT_TIMEOUT_MSECS = 60000;

    /**
     * 锁自动过期毫秒数
     */
    private static final int LOCK_EXPIRE_MSECS = 10000;

    /**
     * 锁自动刷新毫秒数
     */
    private static final int LOCK_REFRESH_MSECS = LOCK_EXPIRE_MSECS / 2;


    /**
     * 轮询启用状态
     */
    private static boolean POLLING_ENABLE = false;

    private static RedisCache redisCache = SpringUtils.getBean(RedisCache.class);


    /**
     * 同步锁
     * @param lockKey
     * @param waitTimeout
     * @return
     */
    public static boolean lock(String lockKey, Integer waitTimeout){
        // 休眠时长（毫秒）
        int sleepTime = 100;
        if(waitTimeout == null){
            waitTimeout = WAIT_TIMEOUT_MSECS; // 60秒
        }
        while (waitTimeout >= 0) {

            // 获取锁
            if (lock(lockKey)){
                return true;
            }

            waitTimeout -= sleepTime;
            if(waitTimeout >= 0){
                Threads.sleep(sleepTime);
            }
        }
        return false;

    }


    /**
     * 同步锁
     * @param lockKey
     * @return
     */
    public static boolean lock(String lockKey){
        Long timestamp = System.currentTimeMillis();
        boolean result =  redisCache.setIfAbsent(lockKey, timestamp, LOCK_EXPIRE_MSECS, TimeUnit.MILLISECONDS);
        if(result){
            LockTime lockTime = new LockTime();
            lockTime.setCreateAt(timestamp);
            lockTime.setRefreshAt(timestamp + LOCK_REFRESH_MSECS);
            lockTime.setExpireAt(PERSIST);
            REFRESH_QUEUE.put(lockKey, lockTime);
            startPolling();
        }
        return result;
    }

    /**
     * 设置过期时间
     * @param lockKey
     * @param timeout
     */
    public static void expire(String lockKey, Integer timeout){
        if(REFRESH_QUEUE.containsKey(lockKey)){
            long timestamp = System.currentTimeMillis();
            LockTime lockTime = REFRESH_QUEUE.get(lockKey);
            lockTime.setRefreshAt(timestamp + timeout * 1000 - LOCK_REFRESH_MSECS);
            lockTime.setExpireAt(timestamp + timeout * 1000);
        }

        redisCache.expire(lockKey, timeout);
    }

    /**
     * 刷新
     * @param lockKey
     * @param lockTime
     */
    public static boolean refresh(String lockKey, LockTime lockTime){
        Long createAt = redisCache.getCacheObject(lockKey);
        if(lockTime.getCreateAt().equals(createAt)){
            long timestamp = System.currentTimeMillis();
            long timeout = LOCK_EXPIRE_MSECS + lockTime.getRefreshAt() - timestamp; // 把过期时间岔开，避免同一时间过期

            boolean result =  redisCache.expire(lockKey, timeout, TimeUnit.MILLISECONDS);
            if(result){
                lockTime.setRefreshAt(timestamp + LOCK_REFRESH_MSECS);
            }
            return result;
        }
        return false;
    }

    /**
     * 解除锁定
     * @param lockKey
     */
    public static void unlock(String lockKey){
        REFRESH_QUEUE.remove(lockKey);
        redisCache.deleteObject(lockKey);
    }

    /**
     * 启动轮询
     */
    public static synchronized void startPolling(){
        if(POLLING_ENABLE){
            return ;
        }

        new Thread(new Runnable() {
            @Override
            public void run() {
                try{
                    while(!REFRESH_QUEUE.isEmpty()){
                        Threads.sleep(LOCK_EXPIRE_MSECS / 10);
                        Long timestamp = System.currentTimeMillis();
                        List<String> removeKeys = new ArrayList<String>();
                        for(Map.Entry<String, LockTime> entry : REFRESH_QUEUE.entrySet()){
                            LockTime lockTime = entry.getValue();

                            // 手动设置过期的不进行刷新
                            if(lockTime.getExpireAt() > 0){
                                if(lockTime.getExpireAt() < timestamp){
                                    REFRESH_QUEUE.remove(entry.getKey());
                                }
                                continue;
                            }

                            if(lockTime.getRefreshAt() > timestamp){
                                continue;
                            }

                            if(!refresh(entry.getKey(), lockTime)){
                                // 刷新失败，删除映射
                                removeKeys.add(entry.getKey());
                            }
                        }
                        for(String key : removeKeys){
                            REFRESH_QUEUE.remove(key);
                        }
                    }
                }
                catch (Exception ex){
                    logger.error(ex.getMessage(), ex);
                }
                finally {
                    POLLING_ENABLE = false;
                }


            }
        }).start();

        // 启用
        POLLING_ENABLE = true;
    }

    public static Map<String, LockTime> getRefreshQueue(){
        return REFRESH_QUEUE;
    }

    /**
     * 锁时间信息
     */
    private static class LockTime{

        /**
         * 创建时间戳
         */
        private Long createAt;

        /**
         * 过期时间戳(-1 表示不过期)
         */
        private Long expireAt;

        /**
         * 需要刷新的时间
         */
        private Long refreshAt;

        public Long getCreateAt() {
            return createAt;
        }

        public void setCreateAt(Long createAt) {
            this.createAt = createAt;
        }

        public Long getExpireAt() {
            return expireAt;
        }

        public void setExpireAt(Long expireAt) {
            this.expireAt = expireAt;
        }

        public Long getRefreshAt() {
            return refreshAt;
        }

        public void setRefreshAt(Long refreshAt) {
            this.refreshAt = refreshAt;
        }
    }
}
